Skip to content

feat(service): LoadBalancer Service with DNS annotation#1426

Merged
wenyingd merged 1 commit into
vmware-tanzu:mainfrom
wenyingd:dns-networkinfo-service
Jun 24, 2026
Merged

feat(service): LoadBalancer Service with DNS annotation#1426
wenyingd merged 1 commit into
vmware-tanzu:mainfrom
wenyingd:dns-networkinfo-service

Conversation

@wenyingd

@wenyingd wenyingd commented May 6, 2026

Copy link
Copy Markdown
Contributor

This PR introduces the core capability to automatically manage DNS records for Kubernetes LoadBalancer Services. By annotating a Service with desired hostnames, the NSX operator will automatically provision and lifecycle the corresponding DNS records in NSX using the allocated External IPs, while strictly enforcing VPC-level DNS zone policies.

The following functions are delivered,

  1. Automated Service DNS Provisioning The operator now watches LoadBalancer Services and automatically creates DNS records in NSX. It reads the nsx.vmware.com/hostname annotation (which supports comma-separated multiple hostnames) and maps them to the dynamically allocated External IPs from the Service's LoadBalancerIngress status.
  2. DNS Zone Policy Enforcement All requested hostnames are strictly validated against the allowed DNS zones configured for the namespace's VPC (via NetworkInfo). The operator ensures that records are only created in permitted zones and automatically rejects unsupported formats like wildcard domains.
  3. Partial Success and Error Isolation When multiple hostnames are requested on a single Service, the system evaluates them independently. Valid hostnames are successfully provisioned even if others fail validation (e.g., domains outside allowed zones). This ensures that a single invalid hostname annotation does not block the entire DNS provisioning process.
  4. Automated Garbage Collection The reconciliation loop features robust, implicit garbage collection. It automatically detects and prunes stale, removed, or disallowed DNS records by comparing the newly validated batch of endpoints against all existing records owned by the Service. This ensures the NSX state perfectly matches the valid desired state.
  5. Decoupled IP and DNS Status Updates Core LoadBalancer functionality is prioritized over DNS. Even if DNS reconciliation encounters errors, the operator ensures the Service's status is still updated with the successfully allocated External IPs. This allows IP-based traffic to continue uninterrupted while users troubleshoot their DNS configurations.
  6. Observability via Conditions The status of DNS provisioning is surfaced directly on the Service object via a DNSReady condition. This provides users with clear, immediate feedback on validation failures or provisioning errors without needing to check operator logs.

AI-Tool-Used: Cursor
AI-Tool-Use-Level: Category 2 (Medium)
AI-Code-Category: Category 1 (Production)

Test Done:

  1. Prepare two DNS zones as below,
{
  "results": [
    {
      "dns_domain_name": "example.com",
      "ttl": 300,
      "resource_type": "ProjectDnsZone",
      "path": "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
      ...
    },
    {
      "dns_domain_name": "cook.com",
      "ttl": 300,
      "resource_type": "ProjectDnsZone",
      "path": "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone2",
      ....
    }
  ],
  "result_count": 2,
  "sort_by": "display_name",
  "sort_ascending": true
}
  1. Create a vSphere Namespace which is configured with DNS zone "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1", and then nsx-operator has sync the DNS domain names of the zone to NetworkInfo CR
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get vpcnetworkconfigurations test1-9e070406-930e-4ac0-a779-a4b8c6718b41 -ojsonpath='{.spec.dnsZones}'
["/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1"]
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get networkinfo test1 -n test1 -o jsonpath='{.allowedDNSDomains}'
["example.com"]
  1. Prepare a LB Service in the namespace
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~/wenying ]# kubectl -n test1 get svc httpbin --show-labels
NAME      TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE     LABELS
httpbin   LoadBalancer   172.24.61.217   192.168.0.8   80:30389/TCP   2m55s   app=httpbin,service.route.lbapi.run.tanzu.vmware.com/gateway-name=httpbin,service.route.lbapi.run.tanzu.vmware.com/gateway-namespace=test1,service.route.lbapi.run.tanzu.vmware.com/type=direct,service=httpbin
  1. Annotate the LB Service with the desired hostname, check a Ready=True condition is added
root@421833fb973aade676c9a60ec986fda1 [ ~ ]#  kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname="my-svc.example.com"
service/httpbin annotated

root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
[{"lastTransitionTime":"2026-06-12T02:22:30Z","message":"","reason":"DNSRecordConfigured","status":"True","type":"Ready"}]

root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
/my-svc_zone1_a
{
  "zone_path" : "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
  "record_name" : "my-svc",
  "record_type" : "A",
  "record_values" : [ "192.168.0.8" ],
  "ttl" : 300,
  "resource_type" : "ProjectDnsRecord",
  "id" : "my-svc_zone1_a",
  "display_name" : "my-svc",
  "tags" : [ {
    "scope" : "nsx-op/cluster",
    "tag" : "8bb2d1d0-c954-437b-872e-a6d50909e141"
  }, {
    "scope" : "nsx-op/version",
    "tag" : "1.0.0"
  }, {
    "scope" : "nsx-op/namespace_uid",
    "tag" : "76ea4b65-7147-4632-a196-cd0aadc51142"
  }, {
    "scope" : "nsx-op/dns_for",
    "tag" : "service"
  }, {
    "scope" : "nsx-op/dns_owner_namespace",
    "tag" : "test1"
  }, {
    "scope" : "nsx-op/dns_owner_name",
    "tag" : "httpbin"
  } ],
  "path" : "/orgs/default/projects/project-quality/dns-records/my-svc_zone1_a",
  "relative_path" : "my-svc_zone1_a",
  "parent_path" : "/orgs/default/projects/project-quality",
  "remote_path" : "",
  "unique_id" : "54e10272-5211-4d32-923e-9d56d06bf52a",
  "realization_id" : "54e10272-5211-4d32-923e-9d56d06bf52a",
  "owner_id" : "ed2f2d7a-f308-4bd3-a1b3-c37d9f7e7935",
  "marked_for_delete" : false,
  "overridden" : false,
  "_system_owned" : false,
  "_protection" : "REQUIRE_OVERRIDE",
  "_create_time" : 1781230950337,
  "_create_user" : "wcp-cluster-user-8bb2d1d0-c954-437b-872e-a6d50909e141-93ff32d9-9ac8-41f8-8d80-06a338a15454",
  "_last_modified_time" : 1781230950337,
  "_last_modified_user" : "wcp-cluster-user-8bb2d1d0-c954-437b-872e-a6d50909e141-93ff32d9-9ac8-41f8-8d80-06a338a15454",
  "_revision" : 0
}
  1. Check Annotate an invalid the hostname in the Service which is not matched by the DNS zone, check a Ready=False condition is added
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname="my-svc.example1.com" --overwrite
service/httpbin annotated
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
[{"lastTransitionTime":"2026-06-12T02:37:50Z","message":"hostname \"my-svc.example1.com\" does not match any allowed DNS domain in the namespace","reason":"DNSRecordFailed","status":"False","type":"Ready"}]

// Check NSX stale records (before update) is deleted,
root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ ],
  "result_count" : 0,
  "sort_by" : "display_name",
  "sort_ascending" : true
}
  1. Update the annotation with a valid hostname back, the Ready condition is updated as true, and the NSX record is created again,
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname="my-svc.example.com" --overwrite
service/httpbin annotated
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
[{"lastTransitionTime":"2026-06-12T03:59:07Z","message":"","reason":"DNSRecordConfigured","status":"True","type":"Ready"}]
root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~#curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ {
    "zone_path" : "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
    "record_name" : "my-svc",
    "record_type" : "A",
    "record_values" : [ "192.168.0.8" ],
    "ttl" : 300,
    "resource_type" : "ProjectDnsRecord",
    "id" : "my-svc_zone1_a",
    "display_name" : "my-svc",
    "tags" : [ {
      "scope" : "nsx-op/cluster",
      "tag" : "8bb2d1d0-c954-437b-872e-a6d50909e141"
    }, {
      "scope" : "nsx-op/version",
      "tag" : "1.0.0"
    }, {
      "scope" : "nsx-op/namespace_uid",
      "tag" : "76ea4b65-7147-4632-a196-cd0aadc51142"
    }, {
      "scope" : "nsx-op/dns_for",
      "tag" : "service"
    }, {
      "scope" : "nsx-op/dns_owner_namespace",
      "tag" : "test1"
    }, {
      "scope" : "nsx-op/dns_owner_name",
      "tag" : "httpbin"
    } ],
    "path" : "/orgs/default/projects/project-quality/dns-records/my-svc_zone1_a",
    "relative_path" : "my-svc_zone1_a",
    "parent_path" : "/orgs/default/projects/project-quality",
    "remote_path" : "",
    "unique_id" : "33ddfe90-b4e0-4362-9fc2-e4d0c47158fc",
    "realization_id" : "33ddfe90-b4e0-4362-9fc2-e4d0c47158fc",
    "owner_id" : "ed2f2d7a-f308-4bd3-a1b3-c37d9f7e7935",
    "marked_for_delete" : false,
    "overridden" : false,
    "_system_owned" : false,
    "_protection" : "REQUIRE_OVERRIDE",
    "_create_time" : 1781236747118,
    "_create_user" : "wcp-cluster-user-8bb2d1d0-c954-437b-872e-a6d50909e141-93ff32d9-9ac8-41f8-8d80-06a338a15454",
    "_last_modified_time" : 1781236747118,
    "_last_modified_user" : "wcp-cluster-user-8bb2d1d0-c954-437b-872e-a6d50909e141-93ff32d9-9ac8-41f8-8d80-06a338a15454",
    "_revision" : 0
  } ],
  "result_count" : 1,
  "sort_by" : "display_name",
  "sort_ascending" : true
}
  1. Remove the annotation, the Ready condition is deleted; NSX DNS record is removed,
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname-
service/httpbin annotated
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# 

root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ ],
  "result_count" : 0,
  "sort_by" : "display_name",
  "sort_ascending" : true
}
  1. Update the annotation with two valid FQDNs, using "," split, two NSX records are created
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname="my-svc.example.com,svc2.example.com" --overwrite
service/httpbin annotated
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
[{"lastTransitionTime":"2026-06-12T04:02:39Z","message":"","reason":"DNSRecordConfigured","status":"True","type":"Ready"}]

root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ {
    "zone_path" : "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
    "record_name" : "my-svc",
    "record_type" : "A",
    "record_values" : [ "192.168.0.8" ],
    "ttl" : 300,
    "resource_type" : "ProjectDnsRecord",
    "id" : "my-svc_zone1_a",
    "display_name" : "my-svc",
    "tags" : [ {
      "scope" : "nsx-op/cluster",
      "tag" : "8bb2d1d0-c954-437b-872e-a6d50909e141"
    }, {
      "scope" : "nsx-op/version",
      "tag" : "1.0.0"
    }, {
      "scope" : "nsx-op/namespace_uid",
      "tag" : "76ea4b65-7147-4632-a196-cd0aadc51142"
    }, {
      "scope" : "nsx-op/dns_for",
      "tag" : "service"
    }, {
      "scope" : "nsx-op/dns_owner_namespace",
      "tag" : "test1"
    }, {
      "scope" : "nsx-op/dns_owner_name",
      "tag" : "httpbin"
    } ],
    "path" : "/orgs/default/projects/project-quality/dns-records/my-svc_zone1_a",
    ...
  }, {
    "zone_path" : "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
    "record_name" : "svc2",
    "record_type" : "A",
    "record_values" : [ "192.168.0.8" ],
    "ttl" : 300,
    "resource_type" : "ProjectDnsRecord",
    "id" : "svc2_zone1_a",
    "display_name" : "svc2",
    "tags" : [ {
      "scope" : "nsx-op/cluster",
      "tag" : "8bb2d1d0-c954-437b-872e-a6d50909e141"
    }, {
      "scope" : "nsx-op/version",
      "tag" : "1.0.0"
    }, {
      "scope" : "nsx-op/namespace_uid",
      "tag" : "76ea4b65-7147-4632-a196-cd0aadc51142"
    }, {
      "scope" : "nsx-op/dns_for",
      "tag" : "service"
    }, {
      "scope" : "nsx-op/dns_owner_namespace",
      "tag" : "test1"
    }, {
      "scope" : "nsx-op/dns_owner_name",
      "tag" : "httpbin"
    } ],
    "path" : "/orgs/default/projects/project-quality/dns-records/svc2_zone1_a",
    "relative_path" : "svc2_zone1_a",
    "parent_path" : "/orgs/default/projects/project-quality",
    ...
  } ],
  "result_count" : 2,
  "sort_by" : "display_name",
  "sort_ascending" : true
}
  1. Update the annotation with two valid FQDNs, adding a white space after the splitter,
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname="my-svc.example.com, svc2.example.com" --overwrite
service/httpbin annotated
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.metadata.annotations}'
{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"httpbin\",\"service\":\"httpbin\"},\"name\":\"httpbin\",\"namespace\":\"test1\"},\"spec\":{\"ports\":[{\"name\":\"http\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"httpbin\"},\"type\":\"LoadBalancer\"}}\n","nsx.vmware.com/hostname":"my-svc.example.com, svc2.example.com"}

root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
[{"lastTransitionTime":"2026-06-12T04:02:39Z","message":"","reason":"DNSRecordConfigured","status":"True","type":"Ready"}]

root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ {
    "zone_path" : "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
    "record_name" : "my-svc",
    "record_type" : "A",
    "record_values" : [ "192.168.0.8" ],
    "ttl" : 300,
    "resource_type" : "ProjectDnsRecord",
    "id" : "my-svc_zone1_a",
    "display_name" : "my-svc",
    "tags" : [ {
      "scope" : "nsx-op/cluster",
      "tag" : "8bb2d1d0-c954-437b-872e-a6d50909e141"
    }, {
      "scope" : "nsx-op/version",
      "tag" : "1.0.0"
    }, {
      "scope" : "nsx-op/namespace_uid",
      "tag" : "76ea4b65-7147-4632-a196-cd0aadc51142"
    }, {
      "scope" : "nsx-op/dns_for",
      "tag" : "service"
    }, {
      "scope" : "nsx-op/dns_owner_namespace",
      "tag" : "test1"
    }, {
      "scope" : "nsx-op/dns_owner_name",
      "tag" : "httpbin"
    } ],
    "path" : "/orgs/default/projects/project-quality/dns-records/my-svc_zone1_a",
    "relative_path" : "my-svc_zone1_a",
    "parent_path" : "/orgs/default/projects/project-quality",
    ....
    "_revision" : 0
  }, {
    "zone_path" : "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
    "record_name" : "svc2",
    "record_type" : "A",
    "record_values" : [ "192.168.0.8" ],
    "ttl" : 300,
    "resource_type" : "ProjectDnsRecord",
    "id" : "svc2_zone1_a",
    "display_name" : "svc2",
    "tags" : [ {
      "scope" : "nsx-op/cluster",
      "tag" : "8bb2d1d0-c954-437b-872e-a6d50909e141"
    }, {
      "scope" : "nsx-op/version",
      "tag" : "1.0.0"
    }, {
      "scope" : "nsx-op/namespace_uid",
      "tag" : "76ea4b65-7147-4632-a196-cd0aadc51142"
    }, {
      "scope" : "nsx-op/dns_for",
      "tag" : "service"
    }, {
      "scope" : "nsx-op/dns_owner_namespace",
      "tag" : "test1"
    }, {
      "scope" : "nsx-op/dns_owner_name",
      "tag" : "httpbin"
    } ],
    "path" : "/orgs/default/projects/project-quality/dns-records/svc2_zone1_a",
    "relative_path" : "svc2_zone1_a",
    "parent_path" : "/orgs/default/projects/project-quality",
    ...
    "_revision" : 0
  } ],
  "result_count" : 2,
  "sort_by" : "display_name",
  "sort_ascending" : true
}
  1. Update the annotation with one valid hostname and one invalid hostname, Ready=False condition is configured, only the valid DNS record is left
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname="my-svc.example.com, svc2.example1.com" --overwrite
service/httpbin annotated
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# 
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.metadata.annotations}'
{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"httpbin\",\"service\":\"httpbin\"},\"name\":\"httpbin\",\"namespace\":\"test1\"},\"spec\":{\"ports\":[{\"name\":\"http\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"httpbin\"},\"type\":\"LoadBalancer\"}}\n","nsx.vmware.com/hostname":"my-svc.example.com, svc2.example1.com"}
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# 
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
[{"lastTransitionTime":"2026-06-12T04:08:29Z","message":"hostname \"svc2.example1.com\" does not match any allowed DNS domain in the namespace","reason":"DNSRecordFailed","status":"False","type":"Ready"}]

root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ {
    "zone_path" : "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
    "record_name" : "my-svc",
    "record_type" : "A",
    "record_values" : [ "192.168.0.8" ],
    "ttl" : 300,
    "resource_type" : "ProjectDnsRecord",
    "id" : "my-svc_zone1_a",
    "display_name" : "my-svc",
    "tags" : [ {
      "scope" : "nsx-op/cluster",
      "tag" : "8bb2d1d0-c954-437b-872e-a6d50909e141"
    }, {
      "scope" : "nsx-op/version",
      "tag" : "1.0.0"
    }, {
      "scope" : "nsx-op/namespace_uid",
      "tag" : "76ea4b65-7147-4632-a196-cd0aadc51142"
    }, {
      "scope" : "nsx-op/dns_for",
      "tag" : "service"
    }, {
      "scope" : "nsx-op/dns_owner_namespace",
      "tag" : "test1"
    }, {
      "scope" : "nsx-op/dns_owner_name",
      "tag" : "httpbin"
    } ],
    "path" : "/orgs/default/projects/project-quality/dns-records/my-svc_zone1_a",
    "relative_path" : "my-svc_zone1_a",
    "parent_path" : "/orgs/default/projects/project-quality",
    ...
    "_last_modified_user" : "wcp-cluster-user-8bb2d1d0-c954-437b-872e-a6d50909e141-93ff32d9-9ac8-41f8-8d80-06a338a15454",
    "_revision" : 0
  } ],
  "result_count" : 1,
  "sort_by" : "display_name",
  "sort_ascending" : true
}
  1. Check the valid hostname can not be requested by other services
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc -n test1 -owide
NAME       TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE     SELECTOR
httpbin    LoadBalancer   172.24.61.217   192.168.0.8   80:30389/TCP   5h59m   app=httpbin
httpbin2   LoadBalancer   172.24.20.41    192.168.0.9   80:31683/TCP   89m     app=httpbin2
// Revert Service httpbin with a valid hostname first
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname="my-svc.example.com" --overwrite
service/httpbin annotated
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
[{"lastTransitionTime":"2026-06-12T07:55:54Z","message":"","reason":"DNSRecordConfigured","status":"True","type":"Ready"}]
// Use the same hostname in Service httpbin2 annotation, the expectation is rejected.
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin2 nsx.vmware.com/hostname="my-svc.example.com" --overwrite
service/httpbin2 annotated
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin2 -n test1 -ojsonpath='{.status.conditions}'
[{"lastTransitionTime":"2026-06-12T07:56:29Z","message":"DNS endpoint validation failed for DNS zone policy: FQDN my-svc.example.com is configured with different values in DNS zone /orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1","reason":"DNSRecordFailed","status":"False","type":"Ready"}]

// NSX Record is created for the first Service
root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ {
    "zone_path" : "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
    "record_name" : "my-svc",
    "record_type" : "A",
    "record_values" : [ "192.168.0.8" ],
    "ttl" : 300,
    "resource_type" : "ProjectDnsRecord",
    "id" : "my-svc_zone1_a",
    "display_name" : "my-svc",
    "tags" : [ {
      "scope" : "nsx-op/cluster",
      "tag" : "8bb2d1d0-c954-437b-872e-a6d50909e141"
    }, {
      "scope" : "nsx-op/version",
      "tag" : "1.0.0"
    }, {
      "scope" : "nsx-op/namespace_uid",
      "tag" : "76ea4b65-7147-4632-a196-cd0aadc51142"
    }, {
      "scope" : "nsx-op/dns_for",
      "tag" : "service"
    }, {
      "scope" : "nsx-op/dns_owner_namespace",
      "tag" : "test1"
    }, {
      "scope" : "nsx-op/dns_owner_name",
      "tag" : "httpbin"
    } ],
    "path" : "/orgs/default/projects/project-quality/dns-records/my-svc_zone1_a",
    "relative_path" : "my-svc_zone1_a",
    "parent_path" : "/orgs/default/projects/project-quality",
    "remote_path" : "",
    "unique_id" : "f5556985-dd9e-4357-84e9-606c566b556a",
    "realization_id" : "f5556985-dd9e-4357-84e9-606c566b556a",
    "owner_id" : "ed2f2d7a-f308-4bd3-a1b3-c37d9f7e7935",
    "marked_for_delete" : false,
    "overridden" : false,
    "_system_owned" : false,
    "_protection" : "REQUIRE_OVERRIDE",
    "_create_time" : 1781250723182,
    "_create_user" : "wcp-cluster-user-8bb2d1d0-c954-437b-872e-a6d50909e141-93ff32d9-9ac8-41f8-8d80-06a338a15454",
    "_last_modified_time" : 1781250723182,
    "_last_modified_user" : "wcp-cluster-user-8bb2d1d0-c954-437b-872e-a6d50909e141-93ff32d9-9ac8-41f8-8d80-06a338a15454",
    "_revision" : 0
  } ],
  "result_count" : 1,
  "sort_by" : "display_name",
  "sort_ascending" : true
}
  1. Annotate Service with "wildcard" FQDN, the request is ignored, and records are cleared
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname="*.my-svc.example.com" --overwrite
service/httpbin annotated
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]#
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.metadata.annotations}'
{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"httpbin\",\"service\":\"httpbin\"},\"name\":\"httpbin\",\"namespace\":\"test1\"},\"spec\":{\"ports\":[{\"name\":\"http\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"httpbin\"},\"type\":\"LoadBalancer\"}}\n","nsx.vmware.com/hostname":"*.my-svc.example.com"}
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]#
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# 
root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ ],
  "result_count" : 0,
  "sort_by" : "display_name",
  "sort_ascending" : true
}
  1. Update the permitted DNS zones in vSphere Namespace, clear the invalid DNS records automatically
// Revert the Service httpbin to annotate with a valid hostname
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl -n test1 annotate svc httpbin nsx.vmware.com/hostname="my-svc.example.com" --overwrite
service/httpbin annotated
root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ {
    "zone_path" : "/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone1",
    "record_name" : "my-svc",
    "record_type" : "A",
    "record_values" : [ "192.168.0.8" ],
    "ttl" : 300,
    "resource_type" : "ProjectDnsRecord",
    "id" : "my-svc_zone1_a",
    "display_name" : "my-svc",
    "tags" : [ {
      "scope" : "nsx-op/cluster",
      "tag" : "8bb2d1d0-c954-437b-872e-a6d50909e141"
    }, {
      "scope" : "nsx-op/version",
      "tag" : "1.0.0"
    }, {
      "scope" : "nsx-op/namespace_uid",
      "tag" : "76ea4b65-7147-4632-a196-cd0aadc51142"
    }, {
      "scope" : "nsx-op/dns_for",
      "tag" : "service"
    }, {
      "scope" : "nsx-op/dns_owner_namespace",
      "tag" : "test1"
    }, {
      "scope" : "nsx-op/dns_owner_name",
      "tag" : "httpbin"
    } ],
    "path" : "/orgs/default/projects/project-quality/dns-records/my-svc_zone1_a",
    "relative_path" : "my-svc_zone1_a",
    "parent_path" : "/orgs/default/projects/project-quality",
    "remote_path" : "",
    "unique_id" : "ffa331c4-bd5c-4a94-8096-947f65dc9305",
    "realization_id" : "ffa331c4-bd5c-4a94-8096-947f65dc9305",
    "owner_id" : "ed2f2d7a-f308-4bd3-a1b3-c37d9f7e7935",
    "marked_for_delete" : false,
    "overridden" : false,
    "_system_owned" : false,
    "_protection" : "REQUIRE_OVERRIDE",
    "_create_time" : 1781251401834,
    "_create_user" : "wcp-cluster-user-8bb2d1d0-c954-437b-872e-a6d50909e141-93ff32d9-9ac8-41f8-8d80-06a338a15454",
    "_last_modified_time" : 1781251401834,
    "_last_modified_user" : "wcp-cluster-user-8bb2d1d0-c954-437b-872e-a6d50909e141-93ff32d9-9ac8-41f8-8d80-06a338a15454",
    "_revision" : 0
  } ],
  "result_count" : 1,
  "sort_by" : "display_name",
  "sort_ascending" : true
}

// Update vSphere Namespace DNS zones and check VpcNetworkConfiguration/NetowrkInfo CRs
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get vpcnetworkconfigurations test1-9e070406-930e-4ac0-a779-a4b8c6718b41 -ojsonpath='{.spec.dnsZones}'
["/orgs/default/projects/project-quality/dns-services/default-dns-service/zones/zone2"]
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get networkinfo test1 -n test1 -o jsonpath='{.allowedDNSDomains}'
["cook.com"]

// Check Service annotation and conditions
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.metadata.annotations}'
{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"httpbin\",\"service\":\"httpbin\"},\"name\":\"httpbin\",\"namespace\":\"test1\"},\"spec\":{\"ports\":[{\"name\":\"http\",\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"httpbin\"},\"type\":\"LoadBalancer\"}}\n","nsx.vmware.com/hostname":"my-svc.example.com"}
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]#
root@423e84c75814f2d2942ea47e4a72a7b3 [ ~ ]# kubectl get svc httpbin -n test1 -ojsonpath='{.status.conditions}'
[{"lastTransitionTime":"2026-06-12T08:05:08Z","message":"hostname \"my-svc.example.com\" does not match any allowed DNS domain in the namespace","reason":"DNSRecordFailed","status":"False","type":"Ready"}]

// DNS record was cleared
root@wd008124-vdnet-nsxmanager-ob-25445005-1-dev-nsxvpc-18854:~# curl -k -u $cred https://10.162.215.111/policy/api/v1/orgs/default/projects/project-quality/dns-records
{
  "results" : [ ],
  "result_count" : 0,
  "sort_by" : "display_name",
  "sort_ascending" : true
}

@wenyingd wenyingd force-pushed the dns-networkinfo-service branch from 4764fcf to b3bbbe8 Compare May 6, 2026 08:58
@wenyingd wenyingd changed the title feat(service): LoadBalancer Service DNS with VPC hostname validation feat(service): LoadBalancer Service DNS with DNS annotation May 6, 2026
@wenyingd wenyingd changed the title feat(service): LoadBalancer Service DNS with DNS annotation feat(service): LoadBalancer Service with DNS annotation May 6, 2026
@codecov-commenter

codecov-commenter commented May 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.27526% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.07%. Comparing base (128342c) to head (bb14d79).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/controllers/service/service_lb_dns.go 84.35% 19 Missing and 9 partials ⚠️
pkg/controllers/service/service_lb_controller.go 72.05% 16 Missing and 3 partials ⚠️
cmd/main.go 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1426      +/-   ##
==========================================
+ Coverage   77.96%   78.07%   +0.10%     
==========================================
  Files         184      186       +2     
  Lines       24907    25129     +222     
==========================================
+ Hits        19419    19619     +200     
- Misses       4179     4194      +15     
- Partials     1309     1316       +7     
Flag Coverage Δ
unit-tests 78.07% <83.27%> (+0.10%) ⬆️
Files with missing lines Coverage Δ
pkg/nsx/services/common/types.go 100.00% <ø> (ø)
pkg/nsx/services/dns/recordservice.go 82.62% <100.00%> (+0.06%) ⬆️
pkg/nsx/services/dns/store.go 94.71% <100.00%> (ø)
pkg/nsx/services/dns/types.go 100.00% <ø> (ø)
pkg/nsx/services/dns/zones.go 89.32% <100.00%> (+0.54%) ⬆️
...g/third_party/externaldns/annotations/hostnames.go 100.00% <100.00%> (ø)
cmd/main.go 0.00% <0.00%> (ø)
pkg/controllers/service/service_lb_controller.go 70.58% <72.05%> (+12.42%) ⬆️
pkg/controllers/service/service_lb_dns.go 84.35% <84.35%> (ø)

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@wenyingd wenyingd force-pushed the dns-networkinfo-service branch 16 times, most recently from 4fcb4bf to 4cfcb3a Compare May 15, 2026 07:55
@wenyingd wenyingd force-pushed the dns-networkinfo-service branch 2 times, most recently from ec1e2bc to d654146 Compare May 28, 2026 08:27
@wenyingd wenyingd force-pushed the dns-networkinfo-service branch 3 times, most recently from 28ccbf7 to 988758e Compare June 5, 2026 07:29
@wenyingd wenyingd requested a review from TaoZou1 June 5, 2026 07:29
@wenyingd wenyingd force-pushed the dns-networkinfo-service branch 3 times, most recently from 01e466f to 9406e73 Compare June 5, 2026 12:06
Comment thread pkg/controllers/service/service_lb_dns.go Outdated
Comment thread pkg/controllers/service/service_lb_dns_test.go
Comment thread pkg/controllers/service/service_lb_controller.go
@TaoZou1

TaoZou1 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Could you also add the case if the nsx.vmware.com/hostname: has invalid input?
Extra commas, spaces, invalid hostnames, wildcards. if there was any prompt for the user?

@wenyingd wenyingd force-pushed the dns-networkinfo-service branch from 9406e73 to 1c640e4 Compare June 10, 2026 03:28
Comment thread pkg/controllers/service/service_lb_controller.go Outdated
Comment thread pkg/controllers/service/service_lb_dns.go
Comment thread pkg/controllers/service/service_lb_dns.go Outdated
@wenyingd wenyingd force-pushed the dns-networkinfo-service branch from 1c640e4 to e1dc0cb Compare June 11, 2026 13:23
Comment thread pkg/controllers/service/service_lb_dns.go Outdated
@wenyingd wenyingd force-pushed the dns-networkinfo-service branch 3 times, most recently from 6bb5887 to 6524644 Compare June 12, 2026 03:38
Comment thread pkg/controllers/service/service_lb_controller.go Outdated
Comment thread pkg/controllers/service/service_lb_controller.go
Comment thread pkg/controllers/service/service_lb_controller.go
Comment thread pkg/controllers/service/service_lb_dns.go
@wenyingd wenyingd force-pushed the dns-networkinfo-service branch from 534fc4d to 95a5b2a Compare June 12, 2026 08:18
Comment thread pkg/nsx/services/dns/store.go
@wenyingd wenyingd force-pushed the dns-networkinfo-service branch from b6085d9 to c2e7db5 Compare June 12, 2026 09:29

@yanjunz97 yanjunz97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM, just 2 nits

Comment thread pkg/controllers/service/service_lb_controller.go Outdated
Comment thread pkg/nsx/services/dns/recordservice.go Outdated
- Reconcile DNS records based on the `nsx.vmware.com/hostname` annotation on
  LoadBalancer Services using VPCNetworkConfiguration allowed DNS zones.
- Support multiple comma-separated FQDNs in the annotation.
- Report DNSRecordReady condition for DNS zone validation errors and
  generic DNS build errors.
@wenyingd wenyingd force-pushed the dns-networkinfo-service branch from c2e7db5 to bb14d79 Compare June 16, 2026 11:43
@wenyingd wenyingd requested a review from yanjunz97 June 16, 2026 11:45
@wenyingd

Copy link
Copy Markdown
Contributor Author

/e2e

@wenyingd

Copy link
Copy Markdown
Contributor Author

/e2e

@wenyingd wenyingd merged commit fb78d18 into vmware-tanzu:main Jun 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants